﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser
{
    #region Using Statements

    using System;
    using System.IO;
    using System.Net;
    using System.Linq;
    using System.Collections.Generic;

    using KLF.Lib.Browser.DOM;
    using KLF.Lib.Browser.DOM.HTML;
    using KLF.Lib.Browser.JavaScriptHelpers;
    using KLF.Lib.Browser.DOM.CSS.SAC.Selectors;
    using KLF.Lib.Browser.Resources;
using KLF.Lib.Browser.Events;

    #endregion

    /// <summary>
    /// Main browser interface
    /// </summary>
    public class WebBrowser : IBrowser
    {
        #region Constructors

        public WebBrowser()
        {
            Options = new BrowserOptions()
            {
                Agent = UserAgent.Firefox9(),
                CacheMaxDiskSize = 10240,
                CacheMaxMemorySize = 1024,
                CacheDirectory = String.Format("{0}\\{1}",
                    Environment.GetEnvironmentVariable("TEMP"), "KLF.Lib.Browser"),
                DownloadDirectory = Environment.GetFolderPath(
                    Environment.SpecialFolder.MyDocuments),
                EnableClr = false,
                EnableCss = true,
                EnableDebug = false,
                EnableDownload = false,
                EnableJavascript = true,
                Timeout = 15000
            };
            Cookies = new CookieContainer();
            Windows = new List<BrowserWindow>();
            AddWindow();
        }

        #endregion

        #region Attributes

        /// <summary>
        /// The active window.
        /// </summary>
        public BrowserWindow ActiveWindow { get; set; }

        /// <summary>
        /// The active document (from the active window).
        /// </summary>
        public HTMLDocument ActiveDocument
        {
            get
            {
                return ActiveWindow != null && ActiveWindow.ActiveDocument != null ?
                    ActiveWindow.ActiveDocument as HTMLDocument : null;
            }
        }

        /// <summary>
        /// Returns the instance of the cache manager.
        /// </summary>
        public CacheManager Cache
        {
            get
            {
                if (_Cache == null)
                    _Cache = new CacheManager(
                        Options.CacheDirectory, 
                        Options.CacheMaxDiskSize, 
                        Options.CacheMaxMemorySize, true);
                return _Cache;
            }
        }
        internal CacheManager _Cache;

        /// <summary>
        /// The cookies collected over the life of this browser instance 
        /// (all windows).
        /// </summary>
        public CookieContainer Cookies { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public BrowserOptions Options { get; set; }

        /// <summary>
        /// The collection of current windows.
        /// </summary>
        public List<BrowserWindow> Windows { get; set; }

        #endregion

        #region Events

        /// <summary>
        /// Fires when a navigation error occurs (post, get, etc).
        /// </summary>
        public event EventHandler<NavigationErrorArgs> OnNavigationError;

        /// <summary>
        /// This event fires when after a block of javascript has executed.
        /// </summary>
        public event EventHandler<ScriptPostExecuteArgs> OnPostExecuteScript;

        /// <summary>
        /// This event fires just before a script block is executed by the DocumentLoader.
        /// </summary>
        public event EventHandler<ScriptPreExecuteArgs> OnPreExecuteScript;

        /// <summary>
        /// This event fires when an exception occurrs during javascript execution.
        /// </summary>
        public event EventHandler<ScriptExceptionArgs> OnScriptException;

        #endregion

        #region Methods

        /// <summary>
        /// Adds a new browser window and makes that window the active window.
        /// </summary>
        public void AddWindow()
        {
            Windows.Add(ActiveWindow = new BrowserWindow(this));
        }

        #endregion

        #region IBrowser Members

        private bool Click(HTMLElement e)
        {
            if (e == null)
                return false;

            e.click();

            return true;
        }

        /// <summary>
        /// Click's on whatever element is in focus.
        /// </summary>
        /// <returns></returns>
        public bool Click()
        {
            return Click(ActiveDocument.activeElement);
        }

        /// <summary>
        /// Click on an element.
        /// </summary>
        /// <param name="s">Criteria to find the element</param>
        /// <returns>True if successful false if not</returns>
        public bool Click(Selector criteria)
        {
            return Click(ActiveDocument.FindFirst(criteria));
        }

        /// <summary>
        /// Returns a FormHelper that wraps a form from the active document.
        /// </summary>
        /// <param name="selector"></param>
        /// <returns></returns>
        public FormWrapper Form(Selector selector)
        {
            AndSelector formCriteria = new AndSelector(new Selector[]
            {
                selector,
                new ElementSelector() { ElementName = HTMLConstants.HTML_TAG_FORM }
            });
            HTMLFormElement form = (HTMLFormElement)ActiveDocument.FindFirst(formCriteria);

            if (form != null)
            {
                return new FormWrapper(form);
            }
            return null;
        }

        /// <summary>
        /// Go back one page in the browser's history (re-issues request).
        /// </summary>
        /// <returns></returns>
        public bool GoBack()
        {
            int n = ActiveWindow.History.HistoryList.IndexOf(ActiveWindow.Location);

            if ((n - 1) < 0)
                return false;
            RequestContext context = new RequestContext(
                ActiveWindow,
                ActiveWindow.History.HistoryList[n - 1])
                {
                    FromHistory = true,
                };
            return ActiveWindow.ExecuteRequest(context);
        }

        /// <summary>
        /// Go forward one page in the browser's history (re-issues request).
        /// </summary>
        /// <returns></returns>
        public bool GoForward()
        {
            int n = ActiveWindow.History.HistoryList.IndexOf(ActiveWindow.Location);

            if (n < 0 || (n + 1) >= ActiveWindow.History.HistoryList.Count)
                return false;

            RequestContext context = new RequestContext(
                ActiveWindow,
                ActiveWindow.History.HistoryList[n + 1])
            {
                FromHistory = true,
            };
            return ActiveWindow.ExecuteRequest(context);
        }

        /// <summary>
        /// Navigate to the URL specified.
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public bool GoTo(string url)
        {
            return GoTo(new Uri(url), Options.Timeout);
        }

        /// <summary>
        /// Navigate to the URI specified.
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        public bool GoTo(Uri uri)
        {
            return GoTo(uri, Options.Timeout);
        }

        /// <summary>
        /// Navigate to the URL specified in the amount of time specified.
        /// </summary>
        /// <param name="url"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public bool GoTo(string url, int timeout)
        {
            return GoTo(new Uri(url), timeout);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public bool GoTo(Uri uri, int timeout)
        {
            return GoTo(uri, timeout, ActiveWindow.Credentials);
        }

        /// <summary>
        /// Navigate to the URL specified, within the given time, and using
        /// the specified user credentials.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="timeout"></param>
        /// <param name="user"></param>
        /// <returns></returns>
        public bool GoTo(Uri uri, int timeout, ICredentials user)
        {
            RequestContext context = new RequestContext(ActiveWindow, uri);

            context.Timeout = timeout;
            context.Credentials = user;
            context.FromHistory = false;

            return ActiveWindow.ExecuteRequest(context);
        }

        /// <summary>
        /// Loads an HTML document from the local filesystem.
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        public bool LoadFile(string filename)
        {
            FileInfo fi = new FileInfo(filename);

            if (fi.Exists)
            {
                Uri uri = new Uri(fi.FullName);
                RequestContext context = new RequestContext(ActiveWindow, uri);

                context.FromHistory = false;

                return ActiveWindow.ExecuteRequest(context);
            }
            return false;
        }

        /// <summary>
        /// Sets a single form value.
        /// </summary>
        /// <param name="criteria">The criteria by which to find the control.</param>
        /// <param name="value">The new value to pass to the control.</param>
        /// <returns>Returns true on success, false if setting the value failed.</returns>
        public bool SetValue(Selector criteria, string value)
        {
            HTMLDocument html = ActiveDocument as HTMLDocument;

            if (html != null)
            {
                List<HTMLElement> matches = html.FindAll(criteria);

                if (matches == null || matches.Count == 0)
                    throw new ArgumentException("Could not locate item specified.");

                foreach (HTMLElement e in matches)
                {
                    HTMLControlBase input = e as HTMLControlBase;
                    if (input != null)
                        input.SimulateInput(value);
                }

                return true;
            }
            return false;
        }

        #endregion

        /// <summary>
        /// Called after a script has executed.
        /// </summary>
        /// <param name="doc">The parent document in which the script lives.</param>
        /// <param name="script">The script that was executing.</param>
        /// <param name="success">True if the script executed without issue, false if it had errors.</param>
        internal void ScriptPostExecute(HTMLDocument doc, HTMLScriptElement script, bool success)
        {
            if (OnPostExecuteScript != null)
                OnPostExecuteScript(this,
                    new ScriptPostExecuteArgs(doc, script, success));
        }

        /// <summary>
        /// Called just before the DocumentLoader executes a script block.
        /// </summary>
        /// <param name="doc">The parent document in which the script lives.</param>
        /// <param name="script">The script that was executing.</param>
        /// <returns>Returns true if the script should execute, false if it should be prevented.</returns>
        internal bool ScriptPreExecute(HTMLDocument doc, HTMLScriptElement script)
        {
            ScriptPreExecuteArgs args = new ScriptPreExecuteArgs(doc, script);

            if (OnPreExecuteScript != null)
                OnPreExecuteScript(this, args);

            return args.ExecuteScript;
        }

        /// <summary>
        /// Fires when an exception occurs while executing a script.
        /// </summary>
        /// <param name="doc">The parent document in which the script lives.</param>
        /// <param name="script">The script that was executing.</param>
        /// <param name="ex">The exception that occurred while executing the script.</param>
        /// <returns>Returns true if the exception was handled, false if it was not.</returns>
        internal bool ScriptError(HTMLDocument doc, HTMLScriptElement script, Exception ex)
        {
            ScriptExceptionArgs args = new ScriptExceptionArgs(doc, script, ex);

            if (OnScriptException != null)
                OnScriptException(this, args);

            return args.Handled;
        }

        internal void Error(RequestContext context, Exception ex)
        {
            NavigationErrorArgs args = new NavigationErrorArgs(context, ex);

            if (OnNavigationError != null)
                OnNavigationError(this, args);
        }
    }
}
